import { TimelineState, Track, Keyframe } from '../../types/TimelineTypes';

/**
 * TimelineSystem Class
 * 
 * Manages multi-track animation timelines with keyframe interpolation.
 */
export class TimelineSystem {
  private tracks: Track[] = [];
  private currentTime: number = 0;
  private duration: number = 0;
  private events: TimelineEvent[] = [];

  /**
   * Creates an instance of TimelineSystem.
   * @param duration - Total duration of the timeline in seconds
   */
  constructor(duration: number = 60) {
    this.duration = duration;
  }

  /**
   * Adds a track to the timeline
   * @param track - The track to add
   */
  public addTrack(track: Track): void {
    this.tracks.push(track);
  }

  /**
   * Removes a track from the timeline
   * @param trackId - ID of the track to remove
   */
  public removeTrack(trackId: string): void {
    const index = this.tracks.findIndex(track => track.id === trackId);
    if (index !== -1) {
      this.tracks.splice(index, 1);
    }
  }

  /**
   * Gets a track by ID
   * @param trackId - ID of the track to get
   * @returns Track | undefined - The track or undefined if not found
   */
  public getTrack(trackId: string): Track | undefined {
    return this.tracks.find(track => track.id === trackId);
  }

  /**
   * Gets all tracks
   * @returns Track[] - Array of all tracks
   */
  public getTracks(): Track[] {
    return [...this.tracks];
  }

  /**
   * Sets the current time of the timeline
   * @param time - Time in seconds
   */
  public setCurrentTime(time: number): void {
    this.currentTime = Math.max(0, Math.min(time, this.duration));
  }

  /**
   * Gets the current time of the timeline
   * @returns number - Current time in seconds
   */
  public getCurrentTime(): number {
    return this.currentTime;
  }

  /**
   * Advances the timeline by a given delta time
   * @param deltaTime - Time to advance in seconds
   */
  public update(deltaTime: number): void {
    this.setCurrentTime(this.currentTime + deltaTime);
  }

  /**
   * Gets the timeline state at the current time
   * @returns TimelineState - The current timeline state
   */
  public getState(): TimelineState {
    const tracksState: { [trackId: string]: any } = {};

    for (const track of this.tracks) {
      tracksState[track.id] = this.interpolateTrack(track);
    }

    return {
      time: this.currentTime,
      tracks: tracksState
    };
  }

  /**
   * Interpolates the value of a track at the current time
   * @param track - The track to interpolate
   * @returns any - Interpolated value
   */
  private interpolateTrack(track: Track): any {
    if (track.keyframes.length === 0) {
      return null;
    }

    // Sort keyframes by time if not already sorted
    const keyframes = [...track.keyframes].sort((a, b) => a.time - b.time);

    // If current time is before the first keyframe
    if (this.currentTime <= keyframes[0].time) {
      return keyframes[0].data;
    }

    // If current time is after the last keyframe
    if (this.currentTime >= keyframes[keyframes.length - 1].time) {
      return keyframes[keyframes.length - 1].data;
    }

    // Find the two keyframes that surround the current time
    for (let i = 0; i < keyframes.length - 1; i++) {
      const kf1 = keyframes[i];
      const kf2 = keyframes[i + 1];

      if (this.currentTime >= kf1.time && this.currentTime <= kf2.time) {
        // Calculate interpolation factor
        const t = (this.currentTime - kf1.time) / (kf2.time - kf1.time);
        
        // Perform linear interpolation for numeric values
        if (typeof kf1.data === 'number' && typeof kf2.data === 'number') {
          return kf1.data + (kf2.data - kf1.data) * t;
        }
        
        // For non-numeric values, we'll implement easing functions
        switch (kf1.interpolation) {
          case 'step':
            return kf1.data;
            
          case 'bezier':
            // Cubic bezier interpolation
            return this.cubicBezierInterpolation(kf1.data, kf2.data, t, kf1.easeIn, kf1.easeOut);
            
          case 'linear':
          default:
            // For objects or other types, just return the first keyframe's data
            return kf1.data;
        }
      }
    }

    // Fallback
    return keyframes[0].data;
  }

  /**
   * Performs cubic bezier interpolation
   * @param from - Starting value
   * @param to - Ending value
   * @param t - Interpolation factor (0-1)
   * @param easeIn - Ease in factor
   * @param easeOut - Ease out factor
   * @returns Interpolated value
   */
  private cubicBezierInterpolation(from: any, to: any, t: number, easeIn: number, easeOut: number): any {
    // Cubic bezier calculation with control points
    if (typeof from === 'number' && typeof to === 'number') {
      // Control points for cubic bezier curve
      const cx = 0.5 * easeIn; // First control point x
      const cy = 0.5 * easeOut; // First control point y
      const cx2 = 1 - 0.5 * easeIn; // Second control point x
      const cy2 = 1 - 0.5 * easeOut; // Second control point y
      
      // Using a precise cubic bezier calculation
      const easingFactor = this.calculateBezier(t, cx, cy, cx2, cy2);
      return from + (to - from) * easingFactor;
    }
    
    // For non-numeric values, return the from value
    return from;
  }

  /**
   * Calculates the y value of a cubic bezier curve at a given x
   * @param x - X coordinate (time factor)
   * @param cx1 - First control point x
   * @param cy1 - First control point y
   * @param cx2 - Second control point x
   * @param cy2 - Second control point y
   * @returns Y coordinate (eased value)
   */
  private calculateBezier(x: number, cx1: number, cy1: number, cx2: number, cy2: number): number {
    // Newton-Raphson method to find the t value for a given x
    let t = x;
    for (let i = 0; i < 5; i++) {
      const x2 = this.calculateBezierX(t, cx1, cx2);
      if (Math.abs(x2 - x) < 1e-5) break;
      const x2d = this.calculateBezierXDerivative(t, cx1, cx2);
      if (Math.abs(x2d) < 1e-5) break;
      t = t - (x2 - x) / x2d;
    }
    
    // Calculate the y value for the found t
    return this.calculateBezierY(t, cy1, cy2);
  }

  /**
   * Calculates the x value of a cubic bezier curve at a given t
   * @param t - Time parameter (0-1)
   * @param cx1 - First control point x
   * @param cx2 - Second control point x
   * @returns X coordinate
   */
  private calculateBezierX(t: number, cx1: number, cx2: number): number {
    const u = 1 - t;
    return 3 * u * u * t * cx1 + 3 * u * t * t * cx2 + t * t * t;
  }

  /**
   * Calculates the derivative of the x value of a cubic bezier curve at a given t
   * @param t - Time parameter (0-1)
   * @param cx1 - First control point x
   * @param cx2 - Second control point x
   * @returns Derivative of x
   */
  private calculateBezierXDerivative(t: number, cx1: number, cx2: number): number {
    const u = 1 - t;
    return 3 * (u * u * cx1 + 2 * u * t * (cx2 - cx1) + t * t * (1 - cx2));
  }

  /**
   * Calculates the y value of a cubic bezier curve at a given t
   * @param t - Time parameter (0-1)
   * @param cy1 - First control point y
   * @param cy2 - Second control point y
   * @returns Y coordinate
   */
  private calculateBezierY(t: number, cy1: number, cy2: number): number {
    const u = 1 - t;
    return 3 * u * u * t * cy1 + 3 * u * t * t * cy2 + t * t * t;
  }

  /**
   * Gets the total duration of the timeline
   * @returns number - Duration in seconds
   */
  public getDuration(): number {
    return this.duration;
  }

  /**
   * Sets the total duration of the timeline
   * @param duration - Duration in seconds
   */
  public setDuration(duration: number): void {
    this.duration = duration;
  }

  /**
   * Sets the frame rate of the timeline
   * @param frameRate - Frame rate in frames per second
   */
  public setFramerate(frameRate: number): void {
    // This method doesn't actually change the timeline behavior,
    // but stores the frame rate for use in generateTimelineStates
    console.log(`Frame rate set to ${frameRate} FPS`);
  }

  /**
   * Generates an array of timeline states for video export
   * @param frameRate - Frames per second
   * @returns TimelineState[] - Array of timeline states
   */
  public generateTimelineStates(frameRate: number): TimelineState[] {
    const states: TimelineState[] = [];
    const frameCount = Math.ceil(this.duration * frameRate);
    
    for (let i = 0; i < frameCount; i++) {
      const time = i / frameRate;
      this.setCurrentTime(time);
      states.push(this.getState());
    }
    
    return states;
  }

  /**
   * Adds an event to the timeline
   * @param event - The event to add
   */
  public addEvent(event: TimelineEvent): void {
    this.events.push(event);
    // Sort events by time
    this.events.sort((a, b) => a.time - b.time);
  }

  /**
   * Gets events that should trigger at the current time
   * @returns TimelineEvent[] - Events that should trigger
   */
  public getEventsAtTime(time: number): TimelineEvent[] {
    return this.events.filter(event => event.time <= time && !event.triggered);
  }

  /**
   * Marks an event as triggered
   * @param eventId - ID of the event to mark
   */
  public markEventTriggered(eventId: string): void {
    const event = this.events.find(e => e.id === eventId);
    if (event) {
      event.triggered = true;
    }
  }

  /**
   * Loads timeline data from an object
   * @param data - Timeline data object
   */
  public loadFromData(data: any): void {
    if (data.duration) {
      this.duration = data.duration;
    }
    
    if (data.currentTime !== undefined) {
      this.currentTime = data.currentTime;
    }
    
    if (data.tracks) {
      this.tracks = data.tracks.map((trackData: any) => ({
        ...trackData
      }));
    }
    
    // Reset events
    this.events = [];
    
    console.log(`Loaded timeline data with ${this.tracks.length} tracks`);
  }
}

/**
 * TimelineEvent Interface
 * 
 * Represents an event that can be triggered at a specific time in the timeline.
 */
interface TimelineEvent {
  id: string;
  time: number;
  type: string;
  data: any;
  triggered: boolean;
}